Vite 插件
VitePress 处于 Vite 环境下,因此天然支持 Vite 插件。
Teek 有过一个想法,那就是将所有功能完全插件化,通过 NPM
下载各个插件来合并成主题:
- 目录页插件
- 归档页插件
- 文章信息插件
- Footer 插件
- ...
这完全是可行的,每个插件都是独立的,支持任何 VitePress 项目。
但是目前没有太多精力去实现这个计划,您可以通过 Teek 的按需引入功能(等价于下载插件),来加载自己需要的功能。
在了解 Vite 插件实现之前,建议您先去 Vite 官方文档了解什么是 Vite。
下面介绍在 VitePress 中自定义 Vite 插件的场景。
Vite 插件基础模板
首先介绍 Vite 插件的基础模板:
importtype{Plugin } from"vite";interfaceOptions{}exportdefaultfunctionVitePluginVitePressTemplate(option:Options={}):Plugin{return{name:"vite-plugin-vitepress-template",};}
Vite 插件本质是一个函数,需要返回一个对象,对象的各个 Key 就是 Vite 提供的钩子,比如 transform
、config
等,我们需要识别这些钩子分别执行了哪部分逻辑,这样才能针对性的实现自己的功能。
Vite 提供了哪些钩子请看官网 插件 API。
扫描项目文件
如果您使用了 Teek 主题,那么在项目启动时,终端会打印:
InjectedSidebarDataSuccessfully.注入侧边栏数据成功!InjectedPermalinksDataSuccessfully.注入永久链接数据成功!InjectedDocAnalysisInfoDataSuccessfully.注入文档分析数据成功!InjectedCataloguesDataSuccessfully.注入目录页数据成功!InjectedpostsDataSuccessfully.注入posts数据成功!
每一行都是一个 Vite 插件输出的内容,这些插件都是去扫描项目的 Markdown 文件,然后生成数据并注入到 VitePress 的 themeConfig
中。
扫描项目文件的目的有如下场景:
- 生成侧边栏:根据 Markdown 文件路径生成侧边栏数据
- 解析 Markdown 文档的
frontmatter
来生成文章信息,或给 Markdown 文件自动添加frontmatter
- 解析 Markdown 文档的内容,生成站点信息功能(总字数、文章字数、阅读时长等)
- ...
这里需要用到 Vite 提供的 config
钩子,在解析 VitePress 配置前会调用该钩子,因此我们在这个钩子里执行扫描项目文件的逻辑,最后将数据注入到 VitePress 的 themeConfig
中。
importtype{Plugin } from"vite";interfaceOptions{}exportdefaultfunctionVitePluginVitePressDemo(option:Options={}):Plugin&{name:string} {return{name:"vitepress-plugin-demo",config(config:any) {const{site:{themeConfig={} },srcDir,} =config.vitepress;constdata=scanProjectFiles(srcDir);themeConfig.demo =data;},};}constscanProjectFiles=(srcDir:string) =>{};
这里就不详细介绍 scanProjectFiles
的逻辑,您可以阅读 Teek 的 Vite 插件源码来了解具体实现。
加载功能组件
开头说的可以将各个功能完全插件化,就是利用插件来往 VitePress 的插槽中插入组件。
Vite 提供的 load
、transform
、resolveId
等钩子,是在访问某个资源的时候被调用,比如在浏览器访问某个页面时,我们可以通过这些钩子拦截到页面的代码,然后进行内容加工再返回给浏览器渲染。
因此当进入 VitePress 页面时,我们可以拦截 VitePress 的 Layout
组件,然后将自己实现的组件插入到插槽中,最后返回给浏览器渲染。
VitePress 提供了哪些插槽请看 布局插槽。
比如自定义一个组件插入到 Layout
的 layout-top
插槽中。
constisESM=() =>{returntypeof__filename ==="undefined"||typeof__dirname ==="undefined";};constgetDirname=() =>{returnisESM() ?dirname(fileURLToPath(import.meta.url)) :__dirname;};constcomponentName="MyComponent";constcomponentFile=`${componentName}.vue`;constaliasComponentFile=`${getDirname()}/components/${componentFile}`;constvirtualModuleId="virtual:my-component-option";constresolvedVirtualModuleId=`\0${virtualModuleId}`;exportfunctionVitePluginVitePressMyNotFound(option:NotFoundOption={}):Plugin&{name:string} {return{name:"vite-plugin-vitepress-my-not-found",config() {return{resolve:{alias:{[`./${componentFile}`]:aliasComponentFile,},},};},resolveId(id:string) {if(id ===virtualModuleId) returnresolvedVirtualModuleId;},load(id:string) {if(id ===resolvedVirtualModuleId) return`export default ${JSON.stringify(option)}`;if(id.endsWith("vitepress/dist/client/theme-default/Layout.vue")) {constcode=readFileSync(id,"utf-8");constslotName="layout-top";constslotPosition=`<slot name="${slotName}" />`;constsetupPosition='<script setup lang="ts">';returncode.replace(slotPosition,`<${componentName}><template #${slotName}>${slotPosition}</template></${componentName}>`).replace(setupPosition,`${setupPosition}\nimport ${componentName} from './${componentFile}'`);}},};}
<scriptsetuplang="ts"name="MyComponent">importoption from"virtual:my-component-option";const{label="myComponent"} ={...option };</script><template><div>{{label }}</div></template>
插件通过虚拟模块将 option
配置传入到 virtual:my-component-option
中,因此可以在组件里引入。虚拟模块的内容请看 Vite 官网 虚拟模块相关说明
上面 index.ts
给出的代码模板具有通用性,你只需要:
- 将
const componentName ="MyComponent";
改为要插入的组件名 - 将
const slotName ="layout-top";
改为要插入的插槽名
为什么不用 transform
钩子?
transform
钩子返回的资源内容已经过 rollup 编译过,不再是源内容,因此无法找到插槽位置,一个解决方案是使用 load
钩子。
unbuild 构建
unbuild 是一个用于构建库和工具的现代构建工具,由 UnJS
团队开发和维护。它旨在简化构建过程,提供高效的打包和构建功能,特别适用于构建 JavaScript 和 TypeScript 项目。
Teek 使用 unbuild 构建 VitePress 插件,这里仅介绍 unbuild 的 entries
配置项,其他 unbuild 的配置项请看 unbuild 文档。
警告
如果插件在 node 环境下运行,需要构建为 js
相关文件,如果在 client
环境下运行,则可以保留 ts
、vue
等文件。
VitePress 的 .vitepress/config.mts
在 node
环境运行,因此 config.mts
文件引入的第三方依赖必须是 js
相关文件,在 .vitepress/theme/index.ts
文件则可以引入 ts
、vue
等不需要构建的文件。
入口文件
如果插件仅只有一个入口文件 index.ts
,则 unbuild 的配置文件内容如下所示:
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:["src/index"],});
等于:
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"rollup",input:"src/index",outDir:"dist"}],});
后者比较灵活,可以指定输出的位置 outDir
。
提示
如果您觉得 input
或 outDir
的 src/index
、dist
不易于阅读,可以改成 ./src/index
和 ./dist
。
Vue 组件
如果插件有 vue 组件,则 unbuild 的配置文件内容如下所示:
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"mkdist",input:"src/components",outDir:"dist/components",pattern:["***.ts"],format:"cjs",loaders:["js"] },{builder:"mkdist",input:"src",outDir:"dist",pattern:["***.css"],loaders:["postcss"] },],});
静态目录
如果插件有一个静态文件目录 assets
需要复制到输出目录下,则 unbuild 的配置文件内容如下所示:
import{defineBuildConfig } from"unbuild";exportdefaultdefineBuildConfig({entries:[{builder:"copy",input:"src/assets",outDir:"./dist/assets"}],});
使用 copy
功能,input
只能是目录。
使用 rollup 插件
如果你需要一些额外的 rollup
插件打包,则 unbuild 的配置文件内容如下所示:
import{defineBuildConfig } from"unbuild";importRollupPlugin from"rollup-plugin";exportdefaultdefineBuildConfig({entries:[{builder:"rollup",input:"src",outDir:"dist"}],hooks:{"rollup:options":(_,options) =>{if(Array.isArray(options.plugins)) options.plugins.push(RollupPlugin);},},});
hooks
是 unbuild 的一个高级配置项,unbuild 会在指定的阶段调用 hooks
中的钩子,和 Vite 插件的钩子函数一样。
比如你希望在构建成功后,将一些文件 copy
到输出目录中,则可以使用 hooks
的 buildEnd
钩子,并安装 fs-extra
工具实现 copy
。
import{defineBuildConfig } from"unbuild";import{copy } from"fs-extra";exportdefaultdefineBuildConfig({entries:["src/index"],hooks:{"build:done":async() =>{awaitcopy("src/xx.d.ts","dist/xx.d.ts");},},});
externals
externals
是一个数组,用于指定不需要构建的依赖包,它将直接从外部引入,而不是构建到输出目录中。
当你使用了第三方依赖 如 vue、vite 等,需要将这些依赖添加到 externals
中,否则它们将被构建到输出目录中,导致依赖非常大。
import{defineBuildConfig } from"unbuild";importRollupPlugin from"rollup-plugin";exportdefaultdefineBuildConfig({externals:["vue","vite"],});
其他项目使用您的插件时,如何确保这些在 externals
被排除的依赖正确安装呢?毕竟没有这些依赖,插件将无法运行。
您可以在 package.json
的 dependencies
中添加这些依赖,这些第三方依赖就会跟随你的插件一起安装到项目里。
提示
devDependencies
是开发依赖,不会随着插件一起安装到项目里,因此需要您斟酌哪些第三方依赖是运行必须的,则放到 dependencies
里,哪些是开发时必须的,则放到 devDependencies
里。